听说因为代码没"对齐"程序就奔了?(深度剖析)
1、来聊聊(轻松一刻)
来深圳这么久确实没有看到过下雪,而今天推荐的这首歌却唱出了广漂的小伙伴不少的心声,"...不下雪的广东,不一样的天空,他们都一样彼此有着破碎的梦...";也许这就是生活本来的模样吧。
好了,不管怎样心中都要充满着热爱!!今天为大家带来嵌入式中关于"对齐"的那些事,比如地址对齐、结构体数据对齐以及一些常用的处理小技巧。
2、嵌入式中的那些"周期"
在之前的《C语言里面嵌入点“机器码”玩一玩》中作者重点把指令与机器码以及数字电路进行了互连,相信大家应该对程序的运行有了一个形象的认识吧。那么执行这些指令的时间节拍到底是这样的呢?这里作者就把时钟周期、机器周期、指令周期等等周期概念跟大家简单的聊一聊。
1)时钟周期
当我们使用的晶振或者频率没有经过倍频处理,那么这时候的时钟周期 = 1/振荡频率;如果经过锁相环进行倍频以后那么这个时候的时钟周期 = 1/系统主频。
2)机器周期
我们都知道我们的CPU需要进行取指令、译码、执行,然后CPU进行每项基础的操作都需要时间,这个时间我们认为是机器周期,那么机器周期一般都会由一个或者多个时钟周期构成。
3)指令周期
需要知道的是每一条指令都是由一个或者多个机器周期构成的,不过现在随着处理器的进步出现了很多单周期的指令,单周期指令执行时间为一个时钟周期。那么对于多周期指令根据指令的复杂程度其执行时间是不一样的,所以作者在之前的说如何测定程序运行时间中提到:对于通过数指令个数来确定程序运行时间是比较麻烦的。
好了,这里对于这几个概念不过多解释了,主要是为了后面字节对齐效率分析进行一个铺垫,顺便简单画个图供大家理解下:
3、结构体内部对齐
小伙伴们对于int类型根据平台不同会存在差异比较熟悉,而对于结构体的大小也可能会因为系统的字节对齐原因产生变化。下面简单体会一下结构体内的字节对齐:
#include <stdio.h>
#include <stdlib.h>
/************************************************
* Fuciton :结构体定义区
* Author :(公众号:最后一个bug)
************************************************/
typedef struct _tag_Test
{
unsigned char byVal1;
int intVal;
unsigned char byVal2;
} stTest;
#pragma pack(1)
typedef struct _tag_Test1
{
unsigned char byVal1;
int intVal;
unsigned char byVal2;
} stTest1;
#pragma pack()
typedef struct _tag_Test2
{
int intVal;
unsigned char byVal1;
unsigned char byVal2;
} stTest2;
/************************************************
* Fuciton :main
* Author :(公众号:最后一个bug)
************************************************/
int main(int argc, char*argv[]) {
printf("sizeof(stTest) : %d\n",sizeof(stTest));
printf("sizeof(stTest1) : %d\n",sizeof(stTest1));
printf("sizeof(stTest2) : %d\n",sizeof(stTest2));
printf("公众号:最后一个bug");
return0;
}
最终数据的结果:
解析一下:从上面的程序来看,int属于4个字节,那么结构体1采用四字节对齐的方式一共就是12个字节,而结构体2,我们通过使用#pragma pack(1)这样来使得结构体1个字节对齐,同时使用#pragma pack()来进行解除一个字节对齐模式,从而刚好占用6个字节,而结构体3仅仅只是相对结构体1进行变量顺序上的交换,却只有用了8个字节。
对于结构体3的解释 : 编译器在为结构体成员分配内存的时候,结构体的第一个成员分配在offset = 0的位置,而第二个成员通过计算其成员本身占用大小与当前字节对齐大小进行对比,如果还能够装满字节对齐大小,便直接存储,否则就需要分配到下一个对齐地址处,这样之前没有使用完的部分就被填充,从而在一定程度上浪费了一定的内存空间,而结构体3后两个成员刚好可以放到4字节对齐地址里面,所以内存空间减少。
所以平时大家也有这样的说法:“把结构体成员中字节占用比较大的放在结构体头部”,这种说法不完全正确,还是要根据成员大小情况具体排列位置,同时对于第二个结构体采用1字节对齐方式的处理办法便能够节省一定的内存,同时也增强了代码的可移植性,不过就是相对比较耗时间,后面作者会解释一下。
4、内存对齐
其实不仅仅只是结构体内部会存在这样的对齐方式,其实对于平时我们分配的全局变量等内存也是存在地址对齐的问题。我们这里想想如果仅仅只是上面的结构体内部成员对齐,而结构体首地址并没有对齐,那从整体上来看结构体内部对齐也就没有什么意义了。
这里作者就来说说内存对齐,我们都知道CPU在访问内存的时候是通过总线来进行访问,不同CPU其总线都有着不同的宽度,比如16位,32位,64位等,位数越高CPU对数据的吞吐量也就越大,那么一部分CPU为了简化设计加快访问速度,都会只能访问对齐地址上的数据,比如说一些16位的CPU仅仅只能访问偶数地址的内存数据。
那么对于跨越在两个对齐区域的多字节数据会如何处理呢?
1)对于支持非对齐地址访问的CPU,一般都会具有对应的非对齐访问指令,通过判断地址是否跨多个对齐区域,然后分别读取多个对齐区域,最后组合以后返回对应数据(如上图所示),这样明显会增加指令的运行时间,降低了CPU的运行效率;有些小伙伴就会问了,我看编译的汇编代码都是执行了一条指令呀,时间应该都是一样的呀?如果你提了这样的问题,记得返回去一下指令周期的定义。
2 )而对于不支持对齐地址访问的CPU,如果我们在程序中访问不对齐的地址,系统就会抛出异常,比如硬件中断、或者段错误等等。同样结构体对齐也要注意这样的访问问题,所以以后大家在发现程序异常"跑死",定位到异常点以后也可以往地址对齐这方面考虑。
说到这里很多小伙伴都会非常疑惑,好像我们平时写嵌入式代码并没有考虑这么多呀,也没发现有什么问题呀?听到你这里好像我没定义一个变量都要小心翼翼了。哈哈,是的,确实我们平时大多数时候都不用考虑,因为我们都使用了配套的编译器,编译器会检测不同的变量类型,然后为我们自动的进行内存分配的对齐处理,同样结构体内部对齐也会处理,不过对于有些指针的处理部分编译器并不会特意提示开发人员,比如说:我们把char*ptr指针转化为int*ptr指针进行++访问,便有可能会出现非对齐地址访问的问题。
5、最后小结
对于内存对齐问题,还有很多需要各位小伙伴注意的,比如代码的可移植性,不同平台的网络通信过程中的处理等等,都需要对其进行考虑和处理。这里对于该问题有个感性认识即可,对于部分问题还是需要具体熟悉芯片内核的处理办法进行综合分析,对于结构体还有很多丰富的操作技巧,后续作者会一一跟大家带来。
好了,这里是公众号:“最后一个bug”,一个为大家打造的技术知识提升基地。同时非常感谢各位小伙伴的支持,我们下期精彩见!
推荐好文 点击蓝色字体即可跳转